using System;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace Gma.UserActivityMonitor
{
	public static partial class HookManager
	{
		/// <summary>
		/// The CallWndProc hook procedure is an application-defined or library-defined callback 
		/// function used with the SetWindowsHookEx function. The HOOKPROC type defines a pointer 
		/// to this callback function. CallWndProc is a placeholder for the application-defined 
		/// or library-defined function name.
		/// </summary>
		/// <param name="nCode">
		/// [in] Specifies whether the hook procedure must process the message. 
		/// If nCode is HC_ACTION, the hook procedure must process the message. 
		/// If nCode is less than zero, the hook procedure must pass the message to the 
		/// CallNextHookEx function without further processing and must return the 
		/// value returned by CallNextHookEx.
		/// </param>
		/// <param name="wParam">
		/// [in] Specifies whether the message was sent by the current thread. 
		/// If the message was sent by the current thread, it is nonzero; otherwise, it is zero. 
		/// </param>
		/// <param name="lParam">
		/// [in] Pointer to a CWPSTRUCT structure that contains details about the message. 
		/// </param>
		/// <returns>
		/// If nCode is less than zero, the hook procedure must return the value returned by CallNextHookEx. 
		/// If nCode is greater than or equal to zero, it is highly recommended that you call CallNextHookEx 
		/// and return the value it returns; otherwise, other applications that have installed WH_CALLWNDPROC 
		/// hooks will not receive hook notifications and may behave incorrectly as a result. If the hook 
		/// procedure does not call CallNextHookEx, the return value should be zero. 
		/// </returns>
		/// <remarks>
		/// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/hooks/hookreference/hookfunctions/callwndproc.asp
		/// </remarks>
		private delegate int HookProc(int nCode, int wParam, IntPtr lParam);

		//##############################################################################
		#region Mouse hook processing

		/// <summary>
		/// This field is not objectively needed but we need to keep a reference on a delegate which will be 
		/// passed to unmanaged code. To avoid GC to clean it up.
		/// When passing delegates to unmanaged code, they must be kept alive by the managed application 
		/// until it is guaranteed that they will never be called.
		/// </summary>
		private static HookProc s_MouseDelegate;

		/// <summary>
		/// Stores the handle to the mouse hook procedure.
		/// </summary>
		private static int s_MouseHookHandle;

		private static int m_OldX;
		private static int m_OldY;

		/// <summary>
		/// A callback function which will be called every Time a mouse activity detected.
		/// </summary>
		/// <param name="nCode">
		/// [in] Specifies whether the hook procedure must process the message. 
		/// If nCode is HC_ACTION, the hook procedure must process the message. 
		/// If nCode is less than zero, the hook procedure must pass the message to the 
		/// CallNextHookEx function without further processing and must return the 
		/// value returned by CallNextHookEx.
		/// </param>
		/// <param name="wParam">
		/// [in] Specifies whether the message was sent by the current thread. 
		/// If the message was sent by the current thread, it is nonzero; otherwise, it is zero. 
		/// </param>
		/// <param name="lParam">
		/// [in] Pointer to a CWPSTRUCT structure that contains details about the message. 
		/// </param>
		/// <returns>
		/// If nCode is less than zero, the hook procedure must return the value returned by CallNextHookEx. 
		/// If nCode is greater than or equal to zero, it is highly recommended that you call CallNextHookEx 
		/// and return the value it returns; otherwise, other applications that have installed WH_CALLWNDPROC 
		/// hooks will not receive hook notifications and may behave incorrectly as a result. If the hook 
		/// procedure does not call CallNextHookEx, the return value should be zero. 
		/// </returns>
		private static int MouseHookProc(int nCode, int wParam, IntPtr lParam)
		{
			if (nCode >= 0)
			{
				//Marshall the data from callback.
				MouseLLHookStruct mouseHookStruct = (MouseLLHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseLLHookStruct));

				//detect button clicked
				MouseButtons button = MouseButtons.None;
				short mouseDelta = 0;
				int clickCount = 0;
				bool mouseDown = false;
				bool mouseUp = false;

				switch (wParam)
				{
					case WM_LBUTTONDOWN:
						mouseDown = true;
						button = MouseButtons.Left;
						clickCount = 1;
						break;
					case WM_LBUTTONUP:
						mouseUp = true;
						button = MouseButtons.Left;
						clickCount = 1;
						break;
					case WM_LBUTTONDBLCLK:
						button = MouseButtons.Left;
						clickCount = 2;
						break;
					case WM_RBUTTONDOWN:
						mouseDown = true;
						button = MouseButtons.Right;
						clickCount = 1;
						break;
					case WM_RBUTTONUP:
						mouseUp = true;
						button = MouseButtons.Right;
						clickCount = 1;
						break;
					case WM_RBUTTONDBLCLK:
						button = MouseButtons.Right;
						clickCount = 2;
						break;
					case WM_MOUSEWHEEL:
						//If the message is WM_MOUSEWHEEL, the high-order word of MouseData member is the wheel delta. 
						//One wheel click is defined as WHEEL_DELTA, which is 120. 
						//(value >> 16) & 0xffff; retrieves the high-order word from the given 32-bit value
						mouseDelta = (short)((mouseHookStruct.MouseData >> 16) & 0xffff);

						//TODO: X BUTTONS (I havent them so was unable to test)
						//If the message is WM_XBUTTONDOWN, WM_XBUTTONUP, WM_XBUTTONDBLCLK, WM_NCXBUTTONDOWN, WM_NCXBUTTONUP, 
						//or WM_NCXBUTTONDBLCLK, the high-order word specifies which X button was pressed or released, 
						//and the low-order word is reserved. This value can be one or more of the following values. 
						//Otherwise, MouseData is not used. 
						break;
				}

				//generate event 
				MouseEventExtArgs e = new MouseEventExtArgs(
												   button,
												   clickCount,
												   mouseHookStruct.Point.X,
												   mouseHookStruct.Point.Y,
												   mouseDelta);

				//Mouse up
				if (s_MouseUp != null && mouseUp)
				{
					s_MouseUp.Invoke(null, e);
				}

				//Mouse down
				if (s_MouseDown != null && mouseDown)
				{
					s_MouseDown.Invoke(null, e);
				}

				//If someone listens to click and a click is heppened
				if (s_MouseClick != null && clickCount > 0)
				{
					s_MouseClick.Invoke(null, e);
				}

				//If someone listens to click and a click is heppened
				if (s_MouseClickExt != null && clickCount > 0)
				{
					s_MouseClickExt.Invoke(null, e);
				}

				//If someone listens to double click and a click is heppened
				if (s_MouseDoubleClick != null && clickCount == 2)
				{
					s_MouseDoubleClick.Invoke(null, e);
				}

				//Wheel was moved
				if (s_MouseWheel != null && mouseDelta != 0)
				{
					s_MouseWheel.Invoke(null, e);
				}

				//If someone listens to move and there was a change in coordinates raise move event
				if ((s_MouseMove != null || s_MouseMoveExt != null) && (m_OldX != mouseHookStruct.Point.X || m_OldY != mouseHookStruct.Point.Y))
				{
					m_OldX = mouseHookStruct.Point.X;
					m_OldY = mouseHookStruct.Point.Y;
					if (s_MouseMove != null)
					{
						s_MouseMove.Invoke(null, e);
					}

					if (s_MouseMoveExt != null)
					{
						s_MouseMoveExt.Invoke(null, e);
					}
				}

				if (e.Handled)
				{
					return -1;
				}
			}

			//call next hook
			return CallNextHookEx(s_MouseHookHandle, nCode, wParam, lParam);
		}

		private static void EnsureSubscribedToGlobalMouseEvents()
		{
			// install Mouse hook only if it is not installed and must be installed
			if (s_MouseHookHandle == 0)
			{
				//See comment of this field. To avoid GC to clean it up.
				s_MouseDelegate = MouseHookProc;
				//install hook
				//s_MouseHookHandle = SetWindowsHookEx(
				//    WH_MOUSE_LL,
				//    s_MouseDelegate,
				//    Marshal.GetHINSTANCE(
				//        Assembly.GetExecutingAssembly().GetModules(true)[0]),
				//    0);

				s_MouseHookHandle = SetWindowsHookEx(WH_MOUSE_LL, s_MouseDelegate,
					Marshal.GetHINSTANCE(Assembly.GetEntryAssembly().GetModules()[0]), 0);

				//If SetWindowsHookEx fails.
				if (s_MouseHookHandle == 0)
				{
					//Returns the error code returned by the last unmanaged function called using platform invoke that has the DllImportAttribute.SetLastError flag set. 
					int errorCode = Marshal.GetLastWin32Error();
					//do cleanup

					//Initializes and throws a new instance of the Win32Exception class with the specified error. 
					throw new Win32Exception(errorCode);
				}
			}
		}

		private static void TryUnsubscribeFromGlobalMouseEvents()
		{
			//if no subsribers are registered unsubsribe from hook
			if (s_MouseClick == null &&
				s_MouseDown == null &&
				s_MouseMove == null &&
				s_MouseUp == null &&
				s_MouseClickExt == null &&
				s_MouseMoveExt == null &&
				s_MouseWheel == null)
			{
				ForceUnsunscribeFromGlobalMouseEvents();
			}
		}

		private static void ForceUnsunscribeFromGlobalMouseEvents()
		{
			if (s_MouseHookHandle != 0)
			{
				//uninstall hook
				int result = UnhookWindowsHookEx(s_MouseHookHandle);
				//reset invalid handle
				s_MouseHookHandle = 0;
				//Free up for GC
				s_MouseDelegate = null;
				//if failed and exception must be thrown
				if (result == 0)
				{
					//Returns the error code returned by the last unmanaged function called using platform invoke that has the DllImportAttribute.SetLastError flag set. 
					int errorCode = Marshal.GetLastWin32Error();
					//Initializes and throws a new instance of the Win32Exception class with the specified error. 
					throw new Win32Exception(errorCode);
				}
			}
		}

		#endregion

		//##############################################################################
		#region Keyboard hook processing

		/// <summary>
		/// This field is not objectively needed but we need to keep a reference on a delegate which will be 
		/// passed to unmanaged code. To avoid GC to clean it up.
		/// When passing delegates to unmanaged code, they must be kept alive by the managed application 
		/// until it is guaranteed that they will never be called.
		/// </summary>
		private static HookProc s_KeyboardDelegate;

		/// <summary>
		/// Stores the handle to the Keyboard hook procedure.
		/// </summary>
		private static int s_KeyboardHookHandle;

		/// <summary>
		/// A callback function which will be called every Time a keyboard activity detected.
		/// </summary>
		/// <param name="nCode">
		/// [in] Specifies whether the hook procedure must process the message. 
		/// If nCode is HC_ACTION, the hook procedure must process the message. 
		/// If nCode is less than zero, the hook procedure must pass the message to the 
		/// CallNextHookEx function without further processing and must return the 
		/// value returned by CallNextHookEx.
		/// </param>
		/// <param name="wParam">
		/// [in] Specifies whether the message was sent by the current thread. 
		/// If the message was sent by the current thread, it is nonzero; otherwise, it is zero. 
		/// </param>
		/// <param name="lParam">
		/// [in] Pointer to a CWPSTRUCT structure that contains details about the message. 
		/// </param>
		/// <returns>
		/// If nCode is less than zero, the hook procedure must return the value returned by CallNextHookEx. 
		/// If nCode is greater than or equal to zero, it is highly recommended that you call CallNextHookEx 
		/// and return the value it returns; otherwise, other applications that have installed WH_CALLWNDPROC 
		/// hooks will not receive hook notifications and may behave incorrectly as a result. If the hook 
		/// procedure does not call CallNextHookEx, the return value should be zero. 
		/// </returns>
		private static int KeyboardHookProc(int nCode, Int32 wParam, IntPtr lParam)
		{
			//indicates if any of underlaing events set e.Handled flag
			bool handled = false;

			if (nCode >= 0)
			{
				//read structure KeyboardHookStruct at lParam
				KeyboardHookStruct MyKeyboardHookStruct = (KeyboardHookStruct)Marshal.PtrToStructure(lParam, typeof(KeyboardHookStruct));
				//raise KeyDown
				if (s_KeyDown != null && (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN))
				{
					Keys keyData = (Keys)MyKeyboardHookStruct.VirtualKeyCode;
					KeyEventArgs e = new KeyEventArgs(keyData);
					s_KeyDown.Invoke(null, e);
					handled = e.Handled;
				}

				// raise KeyPress
				if (s_KeyPress != null && wParam == WM_KEYDOWN)
				{
					bool isDownShift = ((GetKeyState(VK_SHIFT) & 0x80) == 0x80 ? true : false);
					bool isDownCapslock = (GetKeyState(VK_CAPITAL) != 0 ? true : false);

					byte[] keyState = new byte[256];
					GetKeyboardState(keyState);
					byte[] inBuffer = new byte[2];
					if (ToAscii(MyKeyboardHookStruct.VirtualKeyCode,
							  MyKeyboardHookStruct.ScanCode,
							  keyState,
							  inBuffer,
							  MyKeyboardHookStruct.Flags) == 1)
					{
						char key = (char)inBuffer[0];
						if ((isDownCapslock ^ isDownShift) && Char.IsLetter(key))
							key = Char.ToUpper(key);
						KeyPressEventArgs e = new KeyPressEventArgs(key);
						s_KeyPress.Invoke(null, e);
						handled = handled || e.Handled;
					}
				}

				// raise KeyUp
				if (s_KeyUp != null && (wParam == WM_KEYUP || wParam == WM_SYSKEYUP))
				{
					Keys keyData = (Keys)MyKeyboardHookStruct.VirtualKeyCode;
					KeyEventArgs e = new KeyEventArgs(keyData);
					s_KeyUp.Invoke(null, e);
					handled = handled || e.Handled;
				}

			}

			//if event handled in application do not handoff to other listeners
			if (handled)
				return -1;

			//forward to other application
			return CallNextHookEx(s_KeyboardHookHandle, nCode, wParam, lParam);
		}

		private static void EnsureSubscribedToGlobalKeyboardEvents()
		{
			// install Keyboard hook only if it is not installed and must be installed
			if (s_KeyboardHookHandle == 0)
			{
				//See comment of this field. To avoid GC to clean it up.
				s_KeyboardDelegate = KeyboardHookProc;
				//install hook
				s_KeyboardHookHandle = SetWindowsHookEx(
					WH_KEYBOARD_LL,
					s_KeyboardDelegate,
					Marshal.GetHINSTANCE(
						Assembly.GetExecutingAssembly().GetModules()[0]),
					0);
				//If SetWindowsHookEx fails.
				if (s_KeyboardHookHandle == 0)
				{
					//Returns the error code returned by the last unmanaged function called using platform invoke that has the DllImportAttribute.SetLastError flag set. 
					int errorCode = Marshal.GetLastWin32Error();
					//do cleanup

					//Initializes and throws a new instance of the Win32Exception class with the specified error. 
					throw new Win32Exception(errorCode);
				}
			}
		}

		private static void TryUnsubscribeFromGlobalKeyboardEvents()
		{
			//if no subsribers are registered unsubsribe from hook
			if (s_KeyDown == null &&
				s_KeyUp == null &&
				s_KeyPress == null)
			{
				ForceUnsunscribeFromGlobalKeyboardEvents();
			}
		}

		private static void ForceUnsunscribeFromGlobalKeyboardEvents()
		{
			if (s_KeyboardHookHandle != 0)
			{
				//uninstall hook
				int result = UnhookWindowsHookEx(s_KeyboardHookHandle);
				//reset invalid handle
				s_KeyboardHookHandle = 0;
				//Free up for GC
				s_KeyboardDelegate = null;
				//if failed and exception must be thrown
				if (result == 0)
				{
					//Returns the error code returned by the last unmanaged function called using platform invoke that has the DllImportAttribute.SetLastError flag set. 
					int errorCode = Marshal.GetLastWin32Error();
					//Initializes and throws a new instance of the Win32Exception class with the specified error. 
					throw new Win32Exception(errorCode);
				}
			}
		}

		#endregion

	}
}
